// @ts-check
import * as THREE from 'three'
import * as CANNON from 'cannon'
import _ from 'lodash'

class Bullet {
  constructor({
    x,
    y,
    z,
    viewX,
    viewY,
    world
  }) {
    this.speed = 50
    const geometry = new THREE.CubeGeometry(1, 1, 1)
    const material = new THREE.MeshBasicMaterial({ color: 'red' })
    const cube = new THREE.Mesh(geometry, material)
    cube.position.x = x
    cube.position.y = y
    cube.position.z = z
    this.cube = cube
    this.viewX = viewX
    this.viewY = viewY

    // var x = sphereBody.position.x;
    // var y = sphereBody.position.y;
    // var z = sphereBody.position.z;
    var ballBody = new CANNON.Body({ mass: 1 });
    var ballShape = new CANNON.Sphere(0.2);
    var ballGeometry = new THREE.SphereGeometry(ballShape.radius, 32, 32);
    ballBody.addShape(ballShape);
    var ballMesh = new THREE.Mesh(ballGeometry, material);
    world.addBody(ballBody);
    // scene.add(ballMesh);
    ballMesh.castShadow = true;
    ballMesh.receiveShadow = true;
    // balls.push(ballBody);
    // ballMeshes.push(ballMesh);
    // getShootDir(shootDirection);
    const vx = -this.speed * Math.sin(this.viewY)
    const vy = this.speed * Math.sin(this.viewX)
    const vz = -this.speed * Math.cos(this.viewY)


    // this.cube.position.z -= this.speed * Math.cos(this.viewY)
    // this.cube.position.x -= this.speed * Math.sin(this.viewY)
    // this.cube.position.y += this.speed * Math.sin(this.viewX)
    ballBody.velocity.set(vx, vy, vz)

    // Move the ball outside the player sphere
    // x += shootDirection.x * (sphereShape.radius*1.02 + ballShape.radius);
    // y += shootDirection.y * (sphereShape.radius*1.02 + ballShape.radius);
    // z += shootDirection.z * (sphereShape.radius*1.02 + ballShape.radius);
    ballBody.position.set(x, y, z);
    ballMesh.position.set(x, y, z);

    this.body = ballBody
    this.mesh = ballMesh
  }

  next = () => {
    // this.cube.position.z -= this.speed * Math.cos(this.viewY)
    // this.cube.position.x -= this.speed * Math.sin(this.viewY)
    // this.cube.position.y += this.speed * Math.sin(this.viewX)

    // Update ball positions
    this.mesh.position.copy(this.body.position)
    this.mesh.quaternion.copy(this.body.quaternion)
  }
}

class Engine {
  constructor({
    canvas,
    width,
    height,
    onNext
  }) {
    this._moveSpeed = 1
    this._moveSteps = 0
    this._moveDirection = 'forward'
    this._isMoving = false
    this._vy = 0
    this._bullets = []
    this.boxes = []
    this.boxMeshes = []

    this.onNext = onNext
    this.renderer = new THREE.WebGLRenderer({
      canvas
    })

    const scene = new THREE.Scene()
    const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
    const camera2 = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
    camera.rotation.set(0, 0, 0)
    camera2.position.x = 0
    camera2.position.y = 150
    camera2.position.z = 0
    camera2.rotateX(-Math.PI / 2)

    const axesHelper = new THREE.AxesHelper(150)
    scene.add(axesHelper)
    // scene.fog = new THREE.Fog(0x000000, 0, 750)
    // const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 )
    // scene.add(light)
    this._initWorld()

    const geometry = new THREE.CubeGeometry(1, 1, 1)
    const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
    const yawObject = new THREE.Mesh(geometry, material)

    // const yawObject = new THREE.Object3D()
    const pitchObject = new THREE.Object3D()

    pitchObject.add(camera)
    yawObject.position.y = 10
    yawObject.add(pitchObject)
    scene.add(yawObject)
    scene.add(camera2)

    this.yawObject = yawObject
    this.pitchObject = pitchObject
    this.scene = scene
    this.camera = camera
    this.camera2 = camera2

    this.updateSize({ width, height })
    // this.addLineFloor()
    this.addFloor()

    this._currCamera = this.camera
    this._initLight()
    this._addTarget()
  }

  _addTarget = () => {
    // Add boxes
    const world = this.world
    const scene = this.scene
    const material = new THREE.MeshLambertMaterial({ color: 0xdddddd });
    var halfExtents = new CANNON.Vec3(1, 1, 1);
    var boxShape = new CANNON.Box(halfExtents);
    var boxGeometry = new THREE.BoxGeometry(halfExtents.x * 2, halfExtents.y * 2, halfExtents.z * 2);
    for (var i = 0; i < 7; i++) {
      var x = (Math.random() - 0.5) * 20;
      var y = 1 + (Math.random() - 0.5) * 1;
      var z = (Math.random() - 0.5) * 20;
      var boxBody = new CANNON.Body({ mass: 5 });
      boxBody.addShape(boxShape);
      var boxMesh = new THREE.Mesh(boxGeometry, material);
      world.addBody(boxBody);
      scene.add(boxMesh);
      boxBody.position.set(x, y, z);
      boxMesh.position.set(x, y, z);
      boxMesh.castShadow = true;
      boxMesh.receiveShadow = true;
      this.boxes.push(boxBody);
      this.boxMeshes.push(boxMesh);
    }
  }

  _initLight = () => {
    var ambient = new THREE.AmbientLight(0x111111);
    this.scene.add(ambient);

    const light = new THREE.SpotLight(0xffffff);
    light.position.set(10, 30, 20);
    light.target.position.set(0, 0, 0);
    if (true) {
      light.castShadow = true;

      light.shadow.camera.near = 20;
      light.shadow.camera.far = 50;//camera.far;
      light.shadow.camera.fov = 40;
      light.shadow.bias = 0.1
      // light.shadowMapBias = 0.1;
      light.shadowDarkness = 0.7;
      light.shadow.mapSize.width = 2 * 512;
      light.shadow.mapSize.height = 2 * 512;

      //light.shadowCameraVisible = true;
    }
    this.scene.add(light);
  }

  _initWorld = () => {
    const world = new CANNON.World();
    this.world = world
    world.quatNormalizeSkip = 0;
    world.quatNormalizeFast = false;

    var solver = new CANNON.GSSolver();

    world.defaultContactMaterial.contactEquationStiffness = 1e9;
    world.defaultContactMaterial.contactEquationRelaxation = 4;

    solver.iterations = 7;
    solver.tolerance = 0.1;
    var split = true;
    if (split)
      world.solver = new CANNON.SplitSolver(solver);
    else
      world.solver = solver;

    world.gravity.set(0, -20, 0);
    world.broadphase = new CANNON.NaiveBroadphase();

    // Create a slippery material (friction coefficient = 0.0)
    const physicsMaterial = new CANNON.Material("slipperyMaterial")
    var physicsContactMaterial = new CANNON.ContactMaterial(physicsMaterial,
      physicsMaterial,
      0.0, // friction coefficient
      0.3  // restitution
    );
    // We must add the contact materials to the world
    world.addContactMaterial(physicsContactMaterial);

    // Create a sphere
    var mass = 5, radius = 1.3;
    const sphereShape = new CANNON.Sphere(radius);
    const sphereBody = new CANNON.Body({ mass: mass });
    sphereBody.addShape(sphereShape);
    sphereBody.position.set(0, 5, 0);
    sphereBody.linearDamping = 0.9;
    world.addBody(sphereBody);

    // Create a plane
    var groundShape = new CANNON.Plane();
    var groundBody = new CANNON.Body({ mass: 0 });
    groundBody.addShape(groundShape);
    groundBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
    world.addBody(groundBody);
  }

  rotate = (event) => {
    const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0
    const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0

    this.yawObject.rotation.y -= movementX * 0.002
    this.pitchObject.rotation.x -= movementY * 0.002
  }

  get viewY() {
    return this.yawObject.rotation.y
  }

  get viewX() {
    return this.pitchObject.rotation.x
  }

  addLine = (start, end) => {
    const geometry = new THREE.Geometry()
    const material = new THREE.LineBasicMaterial({ color: 'black' })
    geometry.vertices.push(new THREE.Vector3(start.x, start.y, start.z))
    geometry.vertices.push(new THREE.Vector3(end.x, end.y, end.z))
    const line = new THREE.Line(geometry, material)
    this.scene.add(line)
  }

  addLineFloor = () => {
    const step = 10
    const size = 1000
    const offset = 500

    _.times(size / step, i => {
      const z = i * step - offset
      this.addLine({ x: -500, y: 0, z }, { x: 500, y: 0, z })
    })

    _.times(size / step, i => {
      const x = i * step - offset
      this.addLine({ x, y: 0, z: -500 }, { x, y: 0, z: 500 })
    })
  }

  addMassObjects = () => {
    const step = 10
    const size = 1000
    const offset = 500

    _.times(size / step, i => {
      _.times(size / step, j => {
        const x = i * step - offset
        const y = j * step - offset
        const geometry = new THREE.SphereGeometry(1, 1)
        const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
        const cube = new THREE.Mesh(geometry, material)
        cube.position.x = x
        cube.position.z = y
        // cube.position.y = Math.sin(x / 20) * 10
        this.scene.add(cube)
      })
    })
  }

  addFloor = () => {
    // const geometry = new THREE.PlaneGeometry(1000, 1000, 1, 1)
    // const material = new THREE.MeshBasicMaterial({ color: 'white', side: THREE.DoubleSide })
    // const plane = new THREE.Mesh(geometry, material)
    // plane.position.y = -1
    // plane.rotateX(Math.PI / 2)
    // this.scene.add(plane)

    // floor
    const geometry = new THREE.PlaneGeometry(300, 300, 50, 50);
    geometry.applyMatrix(new THREE.Matrix4().makeRotationX(- Math.PI / 2));

    const material = new THREE.MeshLambertMaterial({ color: 0xdddddd });

    const mesh = new THREE.Mesh(geometry, material);
    mesh.castShadow = true;
    mesh.receiveShadow = true;
    this.scene.add(mesh);

    const renderer = this.renderer
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.Soft = true;
    renderer.setSize(window.innerWidth, window.innerHeight);
    // renderer.setClearColor(this.scene.fog.color, 1);
  }

  updateSize = ({ width, height }) => {
    this.renderer.setSize(width, height)
  }

  _move = () => {
    switch (this._moveDirection) {
      case 'forward':
        this.yawObject.position.z -= this._moveSpeed * Math.cos(this.viewY)
        this.yawObject.position.x -= this._moveSpeed * Math.sin(this.viewY)
        break
      case 'backward':
        this.yawObject.position.z += this._moveSpeed * Math.cos(this.viewY)
        this.yawObject.position.x += this._moveSpeed * Math.sin(this.viewY)
        break
      case 'left':
        this.yawObject.position.x -= this._moveSpeed * Math.cos(this.viewY)
        this.yawObject.position.z += this._moveSpeed * Math.sin(this.viewY)
        break
      case 'right':
        this.yawObject.position.x += this._moveSpeed * Math.cos(this.viewY)
        this.yawObject.position.z -= this._moveSpeed * Math.sin(this.viewY)
        break
      default:
        break
    }
    this._moveSteps--
  }

  shoot = () => {
    const bullet = new Bullet({
      x: this.yawObject.position.x,
      y: this.yawObject.position.y,
      z: this.yawObject.position.z,
      viewX: this.viewX,
      viewY: this.viewY,
      world: this.world
    })
    this._bullets.push(bullet)
    this.scene.add(bullet.mesh)
  }

  switchCamera = () => {
    this._currCamera = this._currCamera === this.camera ? this.camera2 : this.camera
  }

  movePlayer = (direction, isMoving) => {
    this._isMoving = isMoving
    if (!isMoving) return
    this._moveDirection = direction
    this._moveSteps = 15
    this._move()
  }

  playerJump = () => {
    this._vy = 6
  }

  next = () => {
    var dt = 1 / 60
    this.world.step(dt)
    this._bullets.forEach(b => {
      b.next()
    })

    if (this._moveSteps > 0 || this._isMoving) {
      this._move()
    }

    this.yawObject.position.y += this._vy
    const isOffGround = this.yawObject.position.y > 10

    if (isOffGround) {
      this._vy -= 0.5
    } else {
      this._vy = 0
      this.yawObject.position.y = 10
    }

    if (this.onNext) this.onNext(this)

    // Update box positions
    const boxes = this.boxes
    const boxMeshes = this.boxMeshes
    for (var i = 0; i < boxes.length; i++) {
      boxMeshes[i].position.copy(boxes[i].position);
      boxMeshes[i].quaternion.copy(boxes[i].quaternion);
    }
  }

  start = () => {
    const animate = () => {
      requestAnimationFrame(animate)
      this.next()
      this.renderer.render(this.scene, this._currCamera)
    }

    animate()
  }
}

export default Engine
